<?php

/*
 * Copyright (C) 2009 - 2011 Pham Cong Dinh
 *
 * This file is part of Spica.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 3 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */

/**
 * This class uses ffmpeg to extract frames from a video file
 *
 * // where ffmpeg is located, such as /usr/sbin/ffmpeg
 * $ffmpeg = '/usr/bin/ffmpeg';
 *
 * // the input video file
 * $video = dirname(__FILE__) . '/sample.avi';
 *
 * // extract one frame at 10% of the length, one at 30% and so on
 * $frames = array('10%', '30%', '50%', '70%', '90%');
 *
 * // set the delay between frames in the output GIF
 * $joiner = new SpicaThumbnailJoiner(50);
 *
 * // loop through the extracted frames and add them to the joiner object
 * foreach (new SpicaThumbnailExtractor($video, $frames, '200x120', $ffmpeg) as $key => $frame) {
 *     $joiner->add($frame);
 * }
 *
 * $joiner->save(dirname(__FILE__).'/'.'out.gif');
 *
 * @category   spica
 * @package    core
 * @subpackage utils
 * @author     Pham Cong Dinh <pcdinh at phpvietnam dot net>
 * @since      Version 0.3
 * @since      May 03, 2009
 * @copyright  Pham Cong Dinh (http://www.phpvietnam.net)
 * @license    http://www.gnu.org/licenses/lgpl-3.0.txt
 * @version    $Id: VideoUtils.php 1869 2011-01-07 18:55:25Z pcdinh $
 */
class SpicaThumbnailExtractor implements Iterator
{
    /**
     * @var string path to ffmpeg binary
     */
    protected $ffmpeg = 'ffmpeg';

    /**
     * Path to video
     *
     * @var string
     */
    protected $video;

    /**
     * Frames extracted from video
     *
     * @var array
     */
    protected $frames = array();

    /**
     * Thumbnail size
     *
     * @var string
     */
    protected $size = '';

    /**
     * Video length
     *
     * @var integer
     */
    protected $duration = 0;

    /**
     * A switch to keep track of the end of the array
     *
     * @var boolean
     */
    private $valid = false;

    /**
     * Constructor
     *
     * @param string $video  path to source video
     * @param array  $frames array of frames extracted [array('10%', '30%', '50%', '70%', '90%')]
     * @param string $size   frame size [format: '320x260' or array(320,260)]
     * @param string $ffmpeg path to ffmpeg binary
     */
    public function __construct($video, $frames = array(), $size = '', $ffmpeg = 'ffmpeg')
    {
        $this->video  = escapeshellarg($video);
        $this->ffmpeg = escapeshellcmd($ffmpeg);
        $this->duration = $this->_getDuration();
        $this->_setSizeParam($size);
        $this->_setFrames($frames);
    }

    /**
     * Parses and sets the frame size args to pass to ffmpeg
     *
     * @param string|array $size frame size [format: '320x260' or array(320,260)]
     */
    private function _setSizeParam($size)
    {
        if (is_array($size) && 2 == count($size))
        {
            $this->size = '-s '.(int)array_shift($size).'x'.(int)array_shift($size);
        }
        elseif (is_string($size) && preg_match('/^\d+x\d+$/', $size))
        {
            $this->size = '-s '.$size;
        }
    }

    /**
     * Init the frames array
     *
     * @param mixed $frames If integer, take a frame every X seconds;
     *                      If array, take a frame for each array value,
     *                      which can be an integer (seconds from start)
     *                      or a string (percent)
     */
    private function _setFrames($frames)
    {
        if (empty($frames))
        {
            // throw exception?
            return;
        }

        if (is_integer($frames))
        {
            // take a frame every X seconds
            $interval = $frames;
            $frames = array();
            for ($pos = 0; $pos < $this->duration; $pos += $interval)
            {
                $frames[] = $pos;
            }
        }

        if (!is_array($frames))
        {
            // throw exception?
            return;
        }

        // init the frames array
        foreach ($frames as $frame)
        {
            $this->frames[$frame] = null;
        }
    }

    /**
     * Gets the video duration
     *
     * @return int
     */
    private function _getDuration()
    {
        $cmd = "{$this->ffmpeg} -i {$this->video} 2>&1";

        if (preg_match('/Duration: ((\d+):(\d+):(\d+))/s', `$cmd`, $time))
        {
            return ($time[2] * 3600) + ($time[3] * 60) + $time[4];
        }

        return 0;
    }

    /**
     * Gets a video frame from a certain point in time
     *
     * @param integer $second seconds from start
     *
     * @return string binary image contents
     */
    private function getFrame($second)
    {
        $image = tempnam('/tmp', 'FRAME_');
        $out = escapeshellarg($image);
        $cmd = "{$this->ffmpeg} -i {$this->video} -deinterlace -an -ss {$second} -t 00:00:01 -r 1 -y {$this->size} -vcodec mjpeg -f mjpeg {$out} 2>&1";
        `$cmd`;
        $frame = file_get_contents($image);
        @unlink($image);
        return $frame;
    }

    /**
     * Gets the second
     *
     * @param mixed $second if integer, it's taken as absolute time in seconds
     *                      from the start, otherwise it's supposed to be a percentual
     * @return int
     */
    private function getSecond($second)
    {
        if (false !== strpos($second, '%'))
        {
            $percent = (int)str_replace('%', '', $second);
            return (int)($percent * $this->duration / 100);
        }

        return (int)$second;
    }

    /**
     * Return the array "pointer" to the first element
     * PHP's reset() returns false if the array has no elements
     *
     * @return void
     */
    public function rewind()
    {
        $this->valid = (false !== reset($this->frames));
    }

    /**
     * Return the current array element
     *
     * @return string binary image contents
     */
    public function current()
    {
        if (null === current($this->frames))
        {
            $k = $this->key();
            $second = $this->getSecond($k);
            $this->frames[$k] = $this->getFrame($second + 1);
        }

        return current($this->frames);
    }

    /**
     * Return the key of the current array element
     *
     * @return mixed
     */
    public function key()
    {
        return key($this->frames);
    }

    /**
     * Move forward by one
     * PHP's next() returns false if there are no more elements
     *
     * @return void
     */
    public function next()
    {
        $this->valid = (false !== next($this->frames));
    }

    /**
     * Is the current element valid?
     *
     * @return boolean
     */
    public function valid()
    {
        return $this->valid;
    }
}

/**
 * This class uses Imagick to join some images into an animated gif
 */
class SpicaThumbnailJoiner
{
    /**
     * @var integer delay between images (in milliseconds)
     */
    protected $delay = 50;

    /**
     * @var array
     */
    protected $images = array();

    /**
     * Constructs an object of <code>SpicaThumbnailJoiner</code>.
     *
     * @param int $delay between images
     */
    public function __construct($delay = 50)
    {
        $this->delay = $delay;
    }

    /**
     * Loads an image from file
     *
     * @param string $filename
     */
    public function addFile($image)
    {
        $this->images[] = file_get_contents($image);
    }

    /**
     * Loads an image
     *
     * @param string $image binary image data
     */
    public function add($image)
    {
        $this->images[] = $image;
    }

    /**
     * Generates the animated gif
     *
     * @return string binary image data
     */
    public function get()
    {
        $animation = new Imagick();
        $animation->setFormat('gif');

        foreach ($this->images as $image)
        {
            $frame = new Imagick();
            $frame->readImageBlob($image);
            $animation->addImage($frame);
            $animation->setImageDelay($this->delay);
        }

        return $animation->getImagesBlob();
    }

    /**
     * Saves the animated gif to file
     *
     * @param string $outfile output file name
     */
    public function save($outfile)
    {
        file_put_contents($outfile, $this->get());
    }
}

?>